from django.db import models
from django.shortcuts import Http404
from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import slugify
from django.core.exceptions import FieldError
from tags.templatetags.sanitize import sanitize
from django import forms

from apps.course.models import Course, CourseForm
from apps.course.functions import course_reverse
from apps.participants.models import CourseParticipant

import logging

logger = logging.getLogger('pet.apps.gradebook.models')

def get_group_for_assignment(self,assignment):
	if assignment.groups:
		ct = ContentType.objects.get_for_model(assignment)
		group = self.assignment_groups.filter(content_type__pk=ct.id, object_id=assignment.id)
		if group:
			return group[0]
	return None
User.add_to_class("get_group_for_assignment",get_group_for_assignment)

class GradeableInterfaces:
	def __init__(self):
		self.gradeables = []
		
	def register_interface(self, app_title, classname):
		self.gradeables.append(classname)
			
	def get_interfaces(self):
		return self.gradeables
	
Gradeables = GradeableInterfaces()

class GradingCategory(models.Model):
	GRADING_CHOICES = (
		('SUM','Summation - sum of individual assignment scores divided by sum of total points for all assignments'),
		('AVERAGE','Average - average percentage grade of all assignments in this category'),
		('WEIGHTED_AVERAGE','Weighted Average - average percentage grade where each assignment has an individual weight')
	)
	GRADING_OPTIONS = (
		('NONE','None'),
		('LOWEST','Drop x lowest grades'),
		('HIGHEST','Keep x highest grades'),
	)
	name = models.CharField(max_length=255)
	weight = models.FloatField(default=1.0)
	course = models.CharField(max_length=512,editable=False)
	number = models.PositiveIntegerField(blank=True,help_text="(Optional) The category number (used for ordering)")
	grading = models.CharField(max_length=20,choices=GRADING_CHOICES,default=GRADING_CHOICES[0][0])
	option = models.CharField(max_length=10,choices=GRADING_OPTIONS,default=GRADING_OPTIONS[0][0])
	option_argument = models.IntegerField(default=0)
	
	def save(self, *args, **kwargs):
		if not self.number:
			self.number = len(GradingCategory.objects.all()) + 1
		super(GradingCategory,self).save(*args,**kwargs)
	def __unicode__(self):
		return self.name
	@staticmethod
	def find(course=None, name=None, keep_queryset=False):
		Objects = GradingCategory.objects.all()
		if course: Objects = Objects.filter(course__contains=course)
		if name: Objects = Objects.filter(name=name)
		if Objects:
			if len(Objects) == 1 and name and not keep_queryset: return Objects[0]
			return Objects
		elif keep_queryset: return Objects
		return None
	@staticmethod
	def find_or_404(course=None, name=None, keep_queryset=False):
		Result = GradingCategory.find(course, name, keep_queryset)
		if not Result:
			logger.http404('GradingCategory not found for course %s, name %s, keep_queryset %s',course,name,keep_queryset,display_caller=True)
			raise Http404
		return Result
class GradingCategoryForm(forms.ModelForm):
	class Meta:
		model = GradingCategory
		exclude = ['name']

from django.utils.encoding import force_unicode
from django.utils.html import conditional_escape
from itertools import chain
from django.utils.safestring import mark_safe
class CheckboxSelectMultipleTable(forms.SelectMultiple):
	def __init__(self, columns, *args, **kwargs):
		self.columns = columns
		super(CheckboxSelectMultipleTable,self).__init__(*args,**kwargs)
	def render(self, name, value, attrs=None, choices=()):
		if value is None: value = []
		has_id = attrs and 'id' in attrs
		final_attrs = self.build_attrs(attrs, name=name)
		output = []
		# Normalize to strings
		str_values = set([force_unicode(v) for v in value])
		for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
			# If an ID attribute was given, add a numeric index as a suffix,
			# so that the checkboxes don't all have the same ID attribute.
			if has_id:
				final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i))
				label_for = u' for="%s"' % final_attrs['id']
			else:
				label_for = ''

			cb = forms.CheckboxInput(final_attrs, check_test=lambda value: value in str_values)
			option_value = force_unicode(option_value)
			rendered_cb = cb.render(name, option_value)
			option_label = conditional_escape(force_unicode(option_label))
			if i % self.columns == 0: output.append("<tr>")
			output.append(u'<td><label%s>%s %s</label></td>' % (label_for, rendered_cb, option_label))
			if i % self.columns == self.columns-1 : output.append("</tr>")
		return mark_safe(u'\n'.join(output))
	
class GradingGroup(models.Model):
	#Generic link to the assignment
	content_type = models.ForeignKey(ContentType,editable=False)
	object_id = models.PositiveIntegerField(editable=False)
	content_object = generic.GenericForeignKey('content_type', 'object_id')
	
	users = models.ManyToManyField(User,related_name="assignment_groups")
	group_name = models.CharField(max_length=256)
	
	def __unicode__(self):
		return "%s - %s" % (self.content_object,self.group_name)
class GradingGroupEditForm(forms.ModelForm):
	class Meta:
		model = GradingGroup
		fields = ['users','group_name']
	def __init__(self, course, *args, **kwargs):
		super(GradingGroupEditForm, self).__init__(*args, **kwargs)
		self.fields["users"].widget = CheckboxSelectMultipleTable(columns=3)
		CPs = CourseParticipant.find(course,keep_queryset=True)
		self.fields["users"].choices = [(user.user.id,"%s, %s" % (user.user.last_name, user.user.first_name)) for user in CPs.order_by("user__last_name","user__first_name").distinct("user__username")]
class GradingGroupCopyForm(forms.Form):
	assignment = forms.ChoiceField(choices=())
	def __init__(self, course, assignment, *args, **kwargs):
		super(GradingGroupCopyForm, self).__init__(*args, **kwargs)
		Assignments = []
		for interface in Gradeables.get_interfaces():
			for item in interface.objects.filter(course__contains=course):
				if item.groups.count() > 0: Assignments.append(item)
		self.fields["assignment"].choices = [("%s;%s"%(ContentType.objects.get_for_model(item).id,item.id),"%s - %s" % (item.category, item.title)) for item in sorted(Assignments,key=lambda item: "%s - %s" % (item.category,item.title)) if item != assignment]
		self.num_assignments = len(self.fields["assignment"].choices)

class GradingSection(models.Model):
	content_type = models.ForeignKey(ContentType,editable=False)
	object_id = models.PositiveIntegerField(editable=False)
	content_object = generic.GenericForeignKey('content_type', 'object_id')
	title = models.CharField(max_length=255, blank=True, null=True, help_text="The name for this section (default is 'Section #')")
	total_points = models.FloatField(default=10.0,verbose_name="Points", help_text="The total number of points achieveable for this section (not counting extra credit)")
	section_number = models.PositiveIntegerField(blank=True,help_text="(Optional) The section number (used for ordering)")

	def __unicode__(self):
		return "%s - %s - %s) %s" % (self.content_object.category, self.content_object.title, self.section_number, self.title)
	def save(self, *args, **kwargs):
		if not self.section_number:
			self.section_number = len(self.content_object.sections.all()) + 1
		super(GradingSection,self).save(*args,**kwargs)
	def get_response(self, user):
		return self.content_object.get_response(section=self,user=user)
	def get_question(self):
		return self.content_object.get_question(self)
class GradingSectionForm(forms.ModelForm):
	class Meta:
		model = GradingSection
		
class GradingSectionGrade(models.Model):
	section = models.ForeignKey(GradingSection, related_name="grades")
	user = models.ForeignKey(User,related_name='grades',blank=True,null=True)
	points = models.FloatField(default=0.0)
	notes = models.TextField(blank=True, null=True)
	excused = models.BooleanField(default=False)
	
	def save(self, *args, **kwargs):
		if not self.user_id:
			raise FieldError('A user must be attached to every GradingSectionGrade')
		super(GradingSectionGrade,self).save(*args,**kwargs)
	def __unicode__(self):
		return "%s - %s" % (self.section, self.user)
	def get_response(self):
		return self.section.get_response(self.user)
	def get_content_type(self):
		return ContentType.objects.get_for_model(self)
	def file_download_check(self,request,file_object,logger):
		# Allow file for this user
		if request.user == self.user: return True
		# Allow file for course instructor or assistant
		TheCourse = Course.RequestCourse(request)
		TheCourseParticipant = CourseParticipant.find(course=TheCourse,user=request.user)
		if TheCourseParticipant and (TheCourseParticipant.status == "INSTRUCTOR" or TheCourseParticipant.status == "ASSISTANT"): return True
		# Reject file
		logger.error("User %s cannot access user %s's grade-related files" % (request.user,self.user))
		return False
class GradingSectionGradeForm(forms.ModelForm):
	def __init__(self, *args, **kwargs):
		super(GradingSectionGradeForm, self).__init__(*args, **kwargs)
		self.fields['points'].widget.attrs['size'] = 1;
		self.fields['notes'].widget.attrs['cols'] = 20;
		self.fields['notes'].widget.attrs['rows'] = 10;
		self.fields['notes'].widget.attrs['class'] = 'mceNoEditor'
	def get_response(self):
		return self.instance.get_response()
	class Meta:
		model = GradingSectionGrade
		exclude = ['section','user']
		
class Graded(models.Model):
	"""
	Abstract class that implements gradability for Teacher's P.E.T.
	
	Required functions:
	Class-based functions:
	@staticmethod
	get_grades(course, username=None, category=None)
	   returns all grades for this type of object in the given course.
		dict[participant][assignment] = grade_object
		Filtered by username or category if given.
	"""
	sections = generic.GenericRelation(GradingSection)
	title = models.CharField(max_length=255)
	title_slug = models.SlugField(max_length=255,blank=True)
	category = models.CharField(max_length=255)
	category_slug = models.SlugField(max_length=255,blank=True)
	total_points = models.FloatField(default=10.0,blank=True,editable=False)
	course = models.CharField(max_length=512)
	weight = models.FloatField(default=1.0)
	groups = generic.GenericRelation(GradingGroup)
	
	def __init__(self, *args, **kwargs):
		if 'auto_primary_section' in kwargs:
			self.auto_primary_section = kwargs.pop('auto_primary_section')
		else: self.auto_primary_section = True
		super(Graded,self).__init__(*args,**kwargs)
		if self.sections.exists():
			self.total_points = 0
			for section in self.sections.all():
				self.total_points += section.total_points
		
	def save(self,*args,**kwargs):
		self.title = sanitize(self.title)
		self.category = sanitize(self.category)
		self.title_slug = slugify(self.title)
		self.category_slug = slugify(self.category)
		super(Graded,self).save(*args,**kwargs)
		if not self.sections.exists() and self.auto_primary_section == True:
			PrimarySection = GradingSection(content_object=self,title=self.title)
			PrimarySection.save() 
	@staticmethod
	def get_grades(course, classname, category_slug=None, title_slug=None):
		Categories = None
		Objects = classname.find(course,keep_queryset=True)
		if Objects:
			Categories = {}
			if category_slug: Objects = Objects.filter(category_slug = category_slug)
			if title_slug: Objects = Objects.filter(title_slug = title_slug)
			for object in Objects:
				if not object.category in Categories: Categories[object.category] = {}
				Categories[object.category][object] = object.total_points
		return Categories
	
	def get_edit_url(self,course):
		raise NotImplementedError("get_edit_url must be implemented in all Graded objects")
	def allow_anonymous_grading(self):
		return True
	class Meta:
		abstract = True
		unique_together = ('title', 'category', 'course')

class GradedEditForm(CourseForm):
	def __init__(self, *args, **kwargs):
		super(GradedEditForm, self).__init__(*args, **kwargs)
		self.fields['title'].widget.attrs['size'] = 60;
		self.fields['category'].widget.attrs['size'] = 60;
		
	class Meta:
		exclude = ['title_slug','category_slug','weight']
class GradedWeightForm(forms.ModelForm):
	class Meta:
		model = Graded
		fields = ['weight']

class ExternalAssignment(Graded):
	@staticmethod
	def find(course=None, category_slug = None, title_slug = None, keep_queryset=False):
		Objects = ExternalAssignment.objects.all()
		if course: Objects = Objects.filter(course__contains=course)
		if title_slug: Objects = Objects.filter(title_slug=title_slug)
		if category_slug: Objects = Objects.filter(category_slug=category_slug)
		if Objects:
			if len(Objects) == 1 and category_slug and title_slug and not keep_queryset: return Objects[0]
			return Objects
		return None
	@staticmethod
	def find_or_404(course, category_slug = None, title_slug = None, keep_queryset = False):
		Result = ExternalAssignment.find(course, category_slug, title_slug, keep_queryset)
		if not Result:
			if logger: logger.error('ExternalAssignments not found for course %s, category_slug %s, title_slug %s, find_first %s',course,category_slug,title_slug,find_first)
			raise Http404
		return Result
	def get_edit_url(self,course):
		return course_reverse('management',course,app='grades',command='edit',arguments='%s&%s'%(self.category_slug,self.title_slug))
	def get_question(self):
		return None
	def get_response(self, question):
		return None
	def allow_anonymous_grading(self):
		return False
Gradeables.register_interface("External Assignment", ExternalAssignment)

class ExternalAssignmentForm(GradedEditForm):
	class Meta(GradedEditForm.Meta):
		model = ExternalAssignment